package com.example.loopyserver.server.impl;

import com.example.loopyserver.common.ResultCode;
import com.example.loopyserver.component.TaskScheduler;
import com.example.loopyserver.component.Validator;
import com.example.loopyserver.dto.TaskDTO;
import com.example.loopyserver.exception.CommonException;
import com.example.loopyserver.server.TaskService;
import com.example.loopyserver.utils.UIDUtils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cglib.core.internal.Function;
import org.springframework.stereotype.Service;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledFuture;


@Service
@RequiredArgsConstructor
public class TaskServiceImpl implements TaskService {
    private final TaskScheduler taskScheduler;
    private final Validator validator;
    private final ModelMapper modelMapper;
    /**
     * task.id -> task
     */
    private final ConcurrentHashMap<String, Task<?>> taskMap = new ConcurrentHashMap<>();
    /**
     * task.type -> waitingQueue
     */
    private final ConcurrentHashMap<String, ConcurrentLinkedQueue<Task<?>>> waitingQueueMap = new ConcurrentHashMap<>();
    @Value("${my-config.task.pop-timeout-millis}")
    private long popTimeoutMillis;

    private ConcurrentLinkedQueue<Task<?>> getWaitingQueue(String type) {
        return waitingQueueMap.computeIfAbsent(type, unused -> new ConcurrentLinkedQueue<>());
    }

    @Override
    public <T> void push(String type, Object args, Class<T> resClass, int timeoutSeconds, Function<T, Boolean> callback) {
        Task<T> task = new Task<>(type, args, resClass, timeoutSeconds, callback);
        taskMap.put(task.id, task);
        push(task);
    }

    @Override
    public <T> T pushAndWaitDone(String type, Object args, Class<T> resClass, int taskTimeoutSeconds, int waitTimeoutSeconds, Function<T, Boolean> callback) {
        Task<T> task = new Task<>(type, args, resClass, taskTimeoutSeconds, callback);
        taskMap.put(task.id, task);
        push(task);
        // 等待任务完成
        synchronized (task) {
            try {
                task.wait(waitTimeoutSeconds * 1000L);
            } catch (InterruptedException ignored) {
            }
            task.end = true;
        }
        task.cancelWaiter();
        taskMap.remove(task.id);
        return task.res;
    }

    private <T> void push(Task<T> task) {
        // 如果没有分配给 poppingQueue，则把任务加入等待队列
        getWaitingQueue(task.type).add(task);
    }

    @Override
    public TaskDTO pop(Set<String> acceptTypes) throws CommonException {
        Task<?> task;
        long latest = System.currentTimeMillis() + popTimeoutMillis;
        while (System.currentTimeMillis() < latest) {
            for (String acceptType : acceptTypes) {
                ConcurrentLinkedQueue<Task<?>> waitingQueue = getWaitingQueue(acceptType);
                while ((task = waitingQueue.poll()) != null) {
                    synchronized (task) {
                        if (task.end) {
                            continue;
                        }
                        task.startWaiter();
                        return task.toDTO();
                    }
                }
            }
            if (latest - System.currentTimeMillis() <= 100) {
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new CommonException(ResultCode.NO_TASK);
            }
        }
        throw new CommonException(ResultCode.NO_TASK);
    }

    @Override
    public void submit(String id, Object res) throws CommonException {
        // 只能提交处于doing状态的任务
        Task<?> task = taskMap.get(id);
        if (task == null) {
            throw new CommonException(ResultCode.TASK_NOT_EXIST);
        }
        // 避免同时提交2个，加锁
        synchronized (task) {
            if (task.end || !task.doing) {
                throw new CommonException(ResultCode.TASK_NOT_EXIST);
            }
            // 检查结果是否合法
            if (!task.setResIfValid(res)) {
                throw new CommonException(ResultCode.INVALID_TASK_RES);
            }
            task.end = true;
            taskMap.remove(id);
            // 可能有线程在等待任务结果
            task.notify();
        }
    }

    @Getter
    private class Task<T> {
        final String id;
        final String type;
        final Object args;
        final Class<T> resClass;
        final int timeoutSeconds;
        final Function<T, Boolean> callback;
        T res = null;
        ScheduledFuture<?> waiter = null;
        boolean doing = false;
        boolean end = false;

        public Task(String type, Object args, Class<T> resClass, int timeoutSeconds, Function<T, Boolean> callback) {
            this.id = UIDUtils.nextStr();
            this.type = type;
            this.args = args;
            this.resClass = resClass;
            this.timeoutSeconds = timeoutSeconds;
            this.callback = callback != null ? callback : unused -> true;
        }

        public void startWaiter() {
            // 如果到了指定时间，任务还处在doing状态，则重新放回任务队列
            doing = true;
            waiter = taskScheduler.addTask(() -> {
                synchronized (this) {
                    boolean pushFlag = !end && doing;
                    doing = false;
                    if (pushFlag) {
                        push(this);
                    }
                }
            }, timeoutSeconds);
        }

        public void cancelWaiter() {
            if (waiter != null) {
                waiter.cancel(false);
            }
            waiter = null;
        }

        public final TaskDTO toDTO() {
            TaskDTO taskDTO = new TaskDTO();
            BeanUtils.copyProperties(this, taskDTO);
            return taskDTO;
        }

        public final boolean setResIfValid(Object res) {
            if (res == null) {
                return resClass == Void.class && callback.apply(null);
            }
            this.res = resClass.isInstance(res) ? resClass.cast(res) : modelMapper.map(res, resClass);
            return validator.validate(this.res) && callback.apply(this.res);
        }
    }
}
